agentmux_srv\backend\reactive/
sanitize.rs1use super::{MAX_MESSAGE_LENGTH, TRUNCATION_SUFFIX};
6
7pub fn sanitize_message(msg: &str) -> String {
15 let mut result = String::with_capacity(msg.len());
16
17 let bytes = msg.as_bytes();
18 let len = bytes.len();
19 let mut i = 0;
20
21 while i < len {
22 let b = bytes[i];
23
24 if b == 0x1b && i + 1 < len {
26 let next = bytes[i + 1];
27
28 if next == b'[' {
30 i += 2;
31 while i < len && !(bytes[i] >= 0x40 && bytes[i] <= 0x7e) {
32 i += 1;
33 }
34 if i < len {
35 i += 1; }
37 continue;
38 }
39
40 if next == b']' {
42 i += 2;
43 while i < len && bytes[i] != 0x07 {
44 if bytes[i] == 0x1b && i + 1 < len && bytes[i + 1] == b'\\' {
46 i += 2;
47 break;
48 }
49 i += 1;
50 }
51 if i < len && bytes[i] == 0x07 {
52 i += 1;
53 }
54 continue;
55 }
56
57 i += 2;
59 continue;
60 }
61
62 if b < 0x20 && b != b'\n' && b != b'\r' && b != b'\t' {
64 i += 1;
65 continue;
66 }
67
68 if b == 0x7f {
70 i += 1;
71 continue;
72 }
73
74 if b < 0x80 {
76 result.push(b as char);
77 i += 1;
78 } else {
79 let seq_len = if b >= 0xF0 {
81 4
82 } else if b >= 0xE0 {
83 3
84 } else if b >= 0xC0 {
85 2
86 } else {
87 i += 1;
89 continue;
90 };
91
92 if i + seq_len <= len {
93 let s = std::str::from_utf8(&bytes[i..i + seq_len]);
94 if let Ok(valid) = s {
95 result.push_str(valid);
96 }
97 i += seq_len;
98 } else {
99 i += 1;
101 }
102 }
103 }
104
105 if result.len() > MAX_MESSAGE_LENGTH {
107 let suffix_len = TRUNCATION_SUFFIX.len();
108 let target = MAX_MESSAGE_LENGTH - suffix_len;
109 let mut end = target;
111 while end > 0 && !result.is_char_boundary(end) {
112 end -= 1;
113 }
114 result.truncate(end);
115 result.push_str(TRUNCATION_SUFFIX);
116 }
117
118 result
119}
120
121pub fn validate_agent_id(agent_id: &str) -> bool {
125 if agent_id.is_empty() || agent_id.len() > 64 {
126 return false;
127 }
128 agent_id
129 .bytes()
130 .all(|b| b.is_ascii_alphanumeric() || b == b'_' || b == b'-')
131}
132
133pub fn format_injected_message(msg: &str, source_agent: Option<&str>, include_source: bool) -> String {
135 if include_source {
136 if let Some(source) = source_agent {
137 if !source.is_empty() {
138 return format!("@{}: {}", source, msg);
139 }
140 }
141 }
142 msg.to_string()
143}
144
145#[allow(dead_code)]
149pub fn validate_agentmux_url(url_str: &str) -> Result<(), String> {
150 if url_str.is_empty() {
151 return Err("URL is empty".to_string());
152 }
153
154 if let Some(scheme_end) = url_str.find("://") {
156 let scheme = &url_str[..scheme_end];
157 let rest = &url_str[scheme_end + 3..];
158
159 match scheme {
160 "https" => Ok(()),
161 "http" => {
162 let authority = rest.split('/').next().unwrap_or("");
164 let host = if authority.starts_with('[') {
165 authority.split(']').next().unwrap_or("")
167 } else {
168 authority.split(':').next().unwrap_or("")
169 };
170 let host_clean = host.trim_start_matches('[').trim_end_matches(']');
172
173 match host_clean {
174 "localhost" | "127.0.0.1" | "::1" => Ok(()),
175 _ => Err(format!(
176 "http URLs only allowed for localhost, got host: {}",
177 host_clean
178 )),
179 }
180 }
181 _ => Err(format!("unsupported URL scheme: {}", scheme)),
182 }
183 } else {
184 Err("invalid URL: missing scheme".to_string())
185 }
186}